iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
佛心分享-SideProject30

我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰系列 第 17

Day17:快取沒有更新!Spring Boot @CacheEvict 來救場

  • 分享至 

  • xImage
  •  

今天這篇文章要來介紹 Spring Boot 中對於快取的使用,會先簡單說明一下我遇到的問題,然後再進入實作解決問題

問題背景

MyMomentum 是一個個人時間管理應用,使用者可以:

  • 創建不同的活動(如:學習、運動、工作)
  • 記錄活動的執行時間
  • 查看各種統計數據(趨勢圖、分布圖、KPI 指標)

問題描述

有一次,我心滿意足的按下結束紀錄,跳出結束時間「已紀錄 30分鐘」,然後馬上點進統計頁面想看看自己達成目標的畫面。

結果畫面上空空如也,我剛剛結束的活動紀錄就像從來沒發生過,真是令人灰心啊。

https://ithelp.ithome.com.tw/upload/images/20250929/20160279tWRvqTGWto.png

技術背景:Spring Cache 框架

其實最根本原因,是後端程式在搜尋時加上了@Cacheable,導致前端取回的資料,不是最新的資料,畫面上也就呈現了個寂寞,那麼

@Cacheable 的作用是什麼?

Spring Cache 提供了 @Cacheable 註解來實現方法的快取:

@Cacheable(value = "activityDistribution", key = "#activityId + '_' + #fromDate + '_' + #toDate + '_' + #grain")
public List<DistributionItem> getActivityDistribution(UUID activityId, Long userId, String fromDate, String toDate, String grain) {
    // 複雜的數據庫查詢邏輯
    // 只有第一次調用時會執行,後續直接返回快取結果
}

優點:

  • 避免重複的複雜數據庫查詢
  • 大幅提升響應速度
  • 減少資料庫負載

缺點:

  • 數據可能不是最新的(導致空空如也)
  • 需要手動管理快取失效

簡單說明完後,讓我們回到問題本身

為什麼快取沒有更新?

1. 快取配置分析

在 StatisticsService 中,我配置了三個快取:

@Service
public class StatisticsService {
    
    @Cacheable(value = "activityDistribution", key = "#activityId + '_' + #fromDate + '_' + #toDate + '_' + #grain")
    public List<DistributionItem> getActivityDistribution(...) { ... }
    
    @Cacheable(value = "activityTrend", key = "#activityId + '_' + #fromDate + '_' + #toDate + '_' + #grain")
    public List<TrendItem> getActivityTrend(...) { ... }
    
    @Cacheable(value = "activityKPIs", key = "#userId + '_' + #activityId + '_' + #fromDate + '_' + #toDate")
    public ActivityKPIs getActivityKPIs(...) { ... }
}

2. 數據流程

https://ithelp.ithome.com.tw/upload/images/20250929/201602795QU1QySfPA.png

也就是說,我們在配置快取後並沒有對應的快取清除邏輯,導致資料庫更新後,API仍在回傳舊的資料

https://ithelp.ithome.com.tw/upload/images/20250929/20160279Jyw3zpaoKj.png

那麼要怎麼樣才能加上對應的快取清除邏輯,確保資料更新呢?

可以使用@CacheEvict,目前主要有兩種使用方式:

方案一:精確清除

@CacheEvict(value = "activityDistribution", key ="#activityId + '_' + #fromDate + '_' + #toDate + '_' + #grain")
public RecordResponse createRecord(Long userId, RecordCreateRequest request) {
    // 只清除特定活動的快取
}

快取怎麼新增的,就怎麼刪除快取。

優點:

  • 只清除相關的快取條目
  • 對其他用戶影響最小
  • 性能影響較小

缺點:

  • 需要精確的 key 匹配邏輯
  • 容易遺漏某些快取條目
  • 實現複雜度高

方案二:全部清除

@CacheEvict(value = "activityDistribution", allEntries = true)
public RecordResponse createRecord(Long userId, RecordCreateRequest request) {
    // 清除整個快取
}

優點:

  • 實現簡單可靠
  • 不會遺漏任何快取
  • 數據絕對一致

缺點:

  • 所有使用者的快取都刪掉(不需要刪的也刪掉)
  • 造成不必要的性能損失

這邊我選方案二,因為:

  1. 系統規模:只有我一個人在用,我自己影響自己,應該不會怎樣吧?
  2. 技術限制:精確清除需要額外實現,可能自己寫個CacheManager之類的,下一篇再做
  3. 開發效率:優先解決問題,後續再優化

以下開始實作:

1. 添加 import

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;

2. 修改(這邊選一個範例呈現)

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class ActivityService {

    /**
     * Update an existing activity
     */
    @Caching(evict = {
        @CacheEvict(value = "activityDistribution", allEntries = true),
        @CacheEvict(value = "activityTrend", allEntries = true),
        @CacheEvict(value = "activityKPIs", allEntries = true)
    })
    public Activity updateActivity(UUID activityId, Long userId, String name, Integer goalTime, String color, String icon) {
        // 原有邏輯保持不變
        // ...
    }
}

註解說明:

  • @Caching: 允許組合多個快取操作
  • @CacheEvict: 清除指定的快取
  • allEntries = true: 清除整個快取
  • beforeInvocation = false: 預設值不用填,只有方法成功執行後才清除快取

加上去之後測試一下,這次馬上能看到最新數據

https://ithelp.ithome.com.tw/upload/images/20250929/20160279G73Y31QIYg.png

未來優化方向

1. 精確清除實現

當系統用戶增加時,可以考慮:

  • 引入 Redis Cache 支援通配符匹配
  • 實現自定義 CacheManager
  • 使用程式化清除特定快取

2. 快取策略優化

  • 根據業務需求調整快取 TTL
  • 實現多層快取策略
  • 監控和優化快取性能

結語:

以上就是今天的內容,看來針對Mymomentum的內容還有很多需要調整的地方,後續首要之務就是來研究一下快取要怎麼處理,可能可以試著導入Redis。

感謝。


上一篇
Day16 回顧篇:八天的心得、問題與下一步調整
下一篇
Day18:Spring Boot 快取優化 — 從 ConcurrentMap 到 Caffeine 精確清除
系列文
我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言